/*
 *    Copyright 2009-2022 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

public class ParamNameResolver {

  public static final String GENERIC_NAME_PREFIX = "param";

  private final boolean useActualParamName;

  /**
   * <p>
   * The key is the index and the value is the name of the parameter.<br />
   * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
   * the parameter index is used. Note that this index could be different from the actual index
   * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
   * </p>
   * <ul>
   * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
   * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
   * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
   * </ul>
   */
  private final SortedMap<Integer, String> names;

  /**
   * 是否存在 @Param 注解
   */
  private boolean hasParamAnnotation;

  /**
   * 参数名解析器的构造器，它将生成一个 参数名索引 -> 参数名 的不可修改的 Map 对象
   *
   * 用途
   * {@link org.apache.ibatis.binding.MapperMethod.MethodSignature#MethodSignature}
   *
   *
   * @param config
   * @param method
   */
  public ParamNameResolver(Configuration config, Method method) {
    // 1. 取全局配置 mybatis.configuration.use-actual-param-name=true（默认是true）
    this.useActualParamName = config.isUseActualParamName();
    // 2. 取方法中参数列表类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 3. 取方法中参数上的注解
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();

    // 4. 注解数量
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    // 5. 遍历方法中的参数类型及在参数上的所有注解
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      // 6. 若参数有类型是 RowBounds 或 ResultHandler 则属于特殊参数类型，不进行操作
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      // 7. 取该参数上注解 @Param 的值
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }

      // 8. @Param 的值为空
      if (name == null) {
        // @Param was not specified.
        if (useActualParamName) {
          // 9. 若使用实际参数名则从参数列表指定位置取参（似乎不能取基础数据类型的参数名，例如：int、double等）
          name = getActualParamName(method, paramIndex);
        }
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          // 10. 若还是取不到参数名则用参数序号表示
          name = String.valueOf(map.size());
        }
      }

      // 11. 将参数名索引 -> 参数名 添加到Map中
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

  private String getActualParamName(Method method, int paramIndex) {
    return ParamNameUtil.getParamNames(method).get(paramIndex);
  }

  private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
  }

  /**
   * Returns parameter names referenced by SQL providers.
   *
   * @return the names
   */
  public String[] getNames() {
    return names.values().toArray(new String[0]);
  }

  /**
   * <p>
   * A single non-special parameter is returned without a name.
   * Multiple parameters are named using the naming rule.
   * In addition to the default names, this method also adds the generic names (param1, param2,
   * ...).
   * 返回一个不带名称的非特殊参数。
   * 多个参数使用命名规则命名。
   * 除了默认名称外，该方法还添加了通用名称(param1, param2，
   * ……)。
   * </p>
   *
   * @param args
   *          the args
   * @return the named params
   */
  public Object getNamedParams(Object[] args) {
    // 1. 取方法的参数个数
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      // 2. 若不包含 @Param注解且仅有一个参数
      Object value = args[names.firstKey()];
      // 3.
      return wrapToMapIfCollection(value, useActualParamName ? names.get(names.firstKey()) : null);
    } else {
      // 构造 ParamMap
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // 添加 参数名 -> 参数值 的映射
        param.put(entry.getValue(), args[entry.getKey()]);
        // add generic param names (param1, param2, ...)
        final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          // 再添加 param1参数名 -> 参数值 的映射
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

  /**
   * Wrap to a {@link ParamMap} if object is {@link Collection} or array.
   *
   * @param object a parameter object
   * @param actualParamName an actual parameter name
   *                        (If specify a name, set an object to {@link ParamMap} with specified name)
   * @return a {@link ParamMap}
   * @since 3.5.5
   *
   * 如果对象是{@link Collection}或数组，则包装为{@link ParamMap}。
   *
   * @param对象一个参数对象
   * @param actualParamName 实际的参数名
   * (如果指定名称，则将对象设置为指定名称的{@link ParamMap})
   * 返回一个{@link ParamMap}
   * @自3.5.5
   */
  public static Object wrapToMapIfCollection(Object object, String actualParamName) {
    if (object instanceof Collection) {
      // 1. 若参数属于 集合 类型，则添加 "collection" -> 参数值 到 ParamMap 中
      ParamMap<Object> map = new ParamMap<>();
      map.put("collection", object);
      // 2. 若参数属于 列表 类型，则添加 "list" -> 参数值 到 ParamMap 中
      if (object instanceof List) {
        map.put("list", object);
      }

      // 3. 若参数存在参数名，则添加 参数名 -> 参数值 到 ParamMap 中
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    } else if (object != null && object.getClass().isArray()) {
      // 4. 若对象是数组类型
      ParamMap<Object> map = new ParamMap<>();
      // 5. 则添加 "array" -> 参数值 到 ParamMap 中
      map.put("array", object);

      // 6. 若参数存在参数名，则添加 参数名 -> 参数值 到 ParamMap 中
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    }
    return object;
  }

}
